<?php
/**
 * Created by PhpStorm.
 * User: Qch
 * Date: 2016/8/14
 * Time: 16:58
 */

namespace J\Web;


class Csrf
{
    /**
     * The name of the HTTP header for sending CSRF token.
     */
    const CSRF_HEADER = 'X-CSRF-Token';
    /**
     * The length of the CSRF token mask.
     */
    const CSRF_MASK_LENGTH = 8;

    /**
     * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to true.
     * When CSRF validation is enabled, forms submitted to an Yii Web application must be originated
     * from the same application. If not, a 400 HTTP exception will be raised.
     *
     * Note, this feature requires that the user client accepts cookie. Also, to use this feature,
     * forms submitted via POST method must contain a hidden input whose name is specified by [[csrfParam]].
     * You may use [[\yii\helpers\Html::beginForm()]] to generate his hidden input.
     *
     * In JavaScript, you may get the values of [[csrfParam]] and [[csrfToken]] via `yii.getCsrfParam()` and
     * `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered.
     * You also need to include CSRF meta tags in your pages by using [[\yii\helpers\Html::csrfMetaTags()]].
     *
     * @see Controller::enableCsrfValidation
     * @see http://en.wikipedia.org/wiki/Cross-site_request_forgery
     */
    protected $enableCsrfValidation = true;

    /**
     * @return boolean
     */
    public function isEnableCsrfValidation()
    {
        return $this->enableCsrfValidation;
    }
    public function getHiddenInput()
    {
        return [$this->csrfParam, $this->getCsrfToken()];
    }
    /**
     * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'.
     * This property is used only when [[enableCsrfValidation]] is true.
     */
    protected $csrfParam = '_csrf';
    protected $_csrfToken;

    /**
     * @var Context
     */
    protected $http;

    /**
     * Csrf constructor.
     * @param Context $http
     */
    public function __construct(Context $http)
    {
        $this->http = $http;
    }


    /**
     * Returns the token used to perform CSRF validation.
     *
     * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/).
     * 通过HTML表单隐藏域 或者 HTTP头部传递
     * @param boolean $regenerate
     * @return string
     */
    protected function getCsrfToken($regenerate = false)
    {
        if ($this->_csrfToken === null || $regenerate) {
            if ($regenerate || ($token = $this->loadCsrfToken()) === null) {
                $token = str_random(32);
                Session::set($this->csrfParam, $token);
            }
            // the mask doesn't need to be very random
            $mask = str_random(self::CSRF_MASK_LENGTH);
            // The + sign may be decoded as blank space later, which will fail the validation
            $this->_csrfToken = str_replace('+', '.', base64_encode($mask . xorTokens($token, $mask)));
        }

        return $this->_csrfToken;
    }

    protected function loadCsrfToken()
    {
        return Session::get($this->csrfParam);
    }

    /**
     * Performs the CSRF validation.
     * The method will compare the CSRF token obtained from a cookie and from a POST field.
     * If they are different, a CSRF attack is detected and a 400 HTTP exception will be raised.
     * This method is called in [[Controller::beforeAction()]].
     * @return boolean whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true.
     */
    public function validateCsrfToken()
    {
        $method = http_method();
        // only validate CSRF token on non-"safe" methods http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
        if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
            return true;
        }

        $trueToken = $this->loadCsrfToken();

        if (!isset($_POST[$this->csrfParam])) return false;
        return $this->validateCsrfTokenInternal($_POST[$this->csrfParam], $trueToken);
    }

    /**
     * Validates CSRF token
     *
     * @param string $token
     * @param string $trueToken
     * @return boolean
     */
    private function validateCsrfTokenInternal($token, $trueToken)
    {
        $token = base64_decode(str_replace('.', '+', $token));
        $n = mb_strlen($token, '8bit');
        if ($n <= self::CSRF_MASK_LENGTH) {
            return false;
        }
        $mask = byteSubStr($token, 0, self::CSRF_MASK_LENGTH);
        $token = byteSubStr($token, self::CSRF_MASK_LENGTH, $n - self::CSRF_MASK_LENGTH);
        $token = xorTokens($mask, $token);

        return $token === $trueToken;
    }
}